﻿// <copyright file="SignalDetectionHelper.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation. All rights reserved.
//  Licensed under the MIT License.
// </copyright>

namespace Mva.Recognition
{
    using System;
    using System.Diagnostics;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.CognitiveServices.Speech;
    using Windows.ApplicationModel.ConversationalAgent;
    using Debug = MvaLib.Diagnostics.Debug;

    /// <summary>
    /// Arguments raised when a signal detection has finished being resolved after being surfaced
    /// by the conversational agent activation runtime.
    /// </summary>
    /// <param name="origin"> The entry point the activation signal originated from. </param>
    public delegate void SignalResolutionEventArgs();


    /// <summary>
    /// Manages State and facilitates the communication between User Speech and DLS Bot Response.
    /// </summary>
    public class SignalDetectionHelper
    {
        private static readonly TimeSpan SignalConfirmationTimeout
            = new TimeSpan(0, 0, 0, 0, 3000);

        private static readonly TimeSpan MinimumSignalSeparation
            = new TimeSpan(0, 0, 0, 0, 1000);

        private static readonly Lazy<SignalDetectionHelper> LazyInstance
            = new Lazy<SignalDetectionHelper>(() =>
            {
                return new SignalDetectionHelper();
            });

        private readonly object keywordLocker;
        private static Timer secondStageFailsafeTimer;
        private Stopwatch secondStageStopwatch;
        private DateTime? lastSignalReceived = null;
        private bool signalNeedsVerification;

        private DialogManager dialogManager = null;
        public IDialogBackend DialogBackend { get; private set; }
        /// <summary>
        /// Initializes a new instance of the <see cref="SignalDetectionHelper"/> class.
        /// </summary>
        private SignalDetectionHelper()
        {
            this.keywordLocker = new object();
        }

        public DialogManager GetDialogManager()
        {
            return dialogManager;
        }

        public void ChangeDialogBackend(IDialogBackend backend)
        {
            if(LocalSettingsHelper.EnableSecondStageKws)
            {
                DialogBackend = backend;
                dialogManager = new DialogManager(backend);
            }
        }

        /// <summary>
        /// Event raised when a signal is confirmed by the conversational agent activation
        /// runtime.
        /// </summary>
        public event SignalResolutionEventArgs KeywordConfirmed;

        /// <summary>
        /// Event raised when a signal is rejected by the conversational agent activation runtime.
        /// </summary>
        public event SignalResolutionEventArgs KeywordRejected;

        /// <summary>
        /// Gets the singleton instance of this class and initializes it if it has not
        /// yet been initialized. Will block until completion if needed.
        /// </summary>
        public static SignalDetectionHelper Instance { get => LazyInstance.Value; }

        /// <summary>
        /// Processes a 1st-stage activation signal as received by the conversational agent
        /// activation runtime.
        /// </summary>
        /// <param name="detectionOrigin"> The entry point through which handler received the activation signal (e.g. via background task or in-app event handler). </param>
        private readonly SemaphoreSlim kwsSemaphore = new SemaphoreSlim(1);

        private bool KwsSemaphoreAcquire()
        {
            return kwsSemaphore.Wait(1);
        }
        private void KwsSemaphoreRelease()
        {
            if (kwsSemaphore.CurrentCount == 0)
            {
                kwsSemaphore.Release();//make sure next activation is available.
            }
        }
        public async void HandleSignalDetection()
        {
            if (KwsSemaphoreAcquire())
            {
                var now = DateTime.Now;
                if (this.lastSignalReceived.HasValue && now.Subtract(this.lastSignalReceived.Value).TotalMilliseconds < MinimumSignalSeparation.TotalMilliseconds)
                {
                    Debug.WriteLine($"Ignoring signal received so soon after previous!");
                    KwsSemaphoreRelease();
                    return;
                }

                this.lastSignalReceived = now;

                var session = await AppSharedState.GetSessionAsync();
                if (session == null || session.Signal == null)
                {
                    Debug.WriteLine($"Ignoring signal due to null session!");
                    KwsSemaphoreRelease();
                    return;
                }

                var signalName = session.Signal?.SignalName;

                Debug.WriteLine($"HandleSignalDetection: '{signalName}'");
                //Debug.WriteLine($"Signal Info: IsSignalVerificationRequired:{session.Signal.IsSignalVerificationRequired}, Context:{session.Signal.SignalContext}, Id:{session.Signal.SignalId}");
                var canSkipVerification = !LocalSettingsHelper.EnableSecondStageKws;

                Debug.WriteLine($"HandleSignalDetection, EnableSecondStageKws:{LocalSettingsHelper.EnableSecondStageKws}, " +
                    $"canSkipVerification:{canSkipVerification}");

                this.signalNeedsVerification = !canSkipVerification;

                if (signalNeedsVerification)
                {
                    EnsureHandlersAsync();
                    await dialogManager.StartConversationAsync(this.signalNeedsVerification).ConfigureAwait(false);
                }

                if (this.signalNeedsVerification)
                {
                    this.StartFailsafeTimer();
                }
                else
                {
                    this.OnKeywordConfirmed(session);
                }
            }
            else
            {
                Debug.WriteLine("Ignore this mva trigger, because the last one didn't finish.");
            }
        }

        private void EnsureHandlersAsync()
        {
            async void OnKeywordRecognized(object sender, string keyword)
            {
                this.StopFailsafeTimer();
                await this.KeywordRecognitionDuringSignalVerification(keyword, isFinal: true).ConfigureAwait(false);
            }
            dialogManager.KeywordRecognized -= OnKeywordRecognized;
            dialogManager.KeywordRecognized += OnKeywordRecognized;
        }


        private async Task KeywordRecognitionDuringSignalVerification(string recognitionText, bool isFinal)
        {
            try
            {
                
               
                var session = await AppSharedState.GetSessionAsync().ConfigureAwait(false);
                if (session == null) return;
                if (session.AgentState != ConversationalAgentState.Detecting)
                {
                    return;
                }

                if (string.IsNullOrEmpty(recognitionText))
                {
                    this.OnKeywordRejected();
                }
                else
                {
                    this.OnKeywordConfirmed(session);
                }
            }
            finally
            {
                KwsSemaphoreRelease();
            }
        }
        private void OnKeywordRejected()
        {
            this.KeywordRejected?.Invoke();

            KwsSemaphoreRelease();
        }

        private void OnKeywordConfirmed(ConversationalAgentSession session)
        {
            Debug.WriteLine(message: $"Confirmed signal received, IsUserAuthenticated={session.IsUserAuthenticated.ToString(null)}");
            if (!session.IsUserAuthenticated)
            {
                // This is a launch over the lock screen. It may be prudent to serialize state
                // and relaunch to ensure a fresh and accurate windowing layout.
                // https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplication.requestrestartasync
            }

            this.KeywordConfirmed?.Invoke();

            KwsSemaphoreRelease();
        }

        private void StartFailsafeTimer()
        {
            this.secondStageStopwatch = Stopwatch.StartNew();
            secondStageFailsafeTimer = new Timer(
                 _ =>
                {
                    var ok = Monitor.TryEnter(keywordLocker);
                    if(ok)
                    {
                        Debug.WriteLine($"Failsafe timer expired; rejecting");
                        this.OnKeywordRejected();
                        Monitor.Exit(keywordLocker);
                    }
                },
                null,
                (int)SignalConfirmationTimeout.TotalMilliseconds,
                Timeout.Infinite);
        }

        private void StopFailsafeTimer()
        {
            var ok = Monitor.TryEnter(keywordLocker);
            if (ok)
            {
                this.secondStageStopwatch?.Stop();
                if (secondStageFailsafeTimer == null)
                {
                    Monitor.Exit(keywordLocker);
                    return;
                }

                //Debug.WriteLine($"Cancelling 2nd-stage failsafe timer. Elapsed: {this.secondStageStopwatch?.ElapsedMilliseconds}ms");
                secondStageFailsafeTimer?.Change(Timeout.Infinite, Timeout.Infinite);
                secondStageFailsafeTimer?.Dispose();
                secondStageFailsafeTimer = null;

                Monitor.Exit(keywordLocker);
            }
        }
    }
}
